﻿namespace Chapter6

  (*
  Mixing 2D and 3D Rendering ( ** )

  In this section we augment our 3D scene seen above with a few tricks that use the SpriteBatch.
  We will add a rotating nebula over a stars background (two beautiful Hubble images) and we will add pulsating lasers between the fighting ships.
  *)
  module With2D =

    open System
    open System.Collections.Generic
    open Microsoft.Xna.Framework
    open Microsoft.Xna.Framework.Graphics
    open Microsoft.Xna.Framework.Input
    open Chapter5

    type Chapter2.Math.Vector2< [<Measure>] 'u >
      with
        member this.ToXNAVector = Vector2(this.X |> float32, this.Y |> float32)

    type Game3D() as this = 
      inherit Game()
      let graphics = new GraphicsDeviceManager(this)
      (*
      In this game we will setup the GraphicsDeviceManager so that it initializes the application with:
      - a fullscreen window
      - a high resolution of 1280x080
      *)
      do graphics.GraphicsProfile           <- GraphicsProfile.HiDef
      do graphics.IsFullScreen              <- true
      do graphics.PreferredBackBufferWidth  <- 1280
      do graphics.PreferredBackBufferHeight <- 800

      (*
      We will use a sprite batch to render the 2D background and the lasers;
      we will also use a ready-made shader, called BasicEffect, to render with the sprite batch in 3D.
      The idea is that we will have the world, view and projection matrices applied to the draw operations of the sprite batch,
      so that the quads rendered by the sprite batch are in the same 3D space of the rest of the scene and not in 2D screen space:
      *)
      let mutable sprite_batch   : SpriteBatch = null
      let mutable basic_effect   : BasicEffect = null

      (*
      We also use two textures for the background and one for lasers:
      *)
      let mutable stars  : Texture2D = null
      let mutable nebula : Texture2D = null
      let mutable laser  : Texture2D = null

      let mutable police_ship    : Model = null
      let mutable pirate_ship    : Model = null
      let mutable cargo_ship     : Model = null
      let mutable police_station : Model = null

      let mutable zoom = 1.0f
      let mutable center = Vector3.Zero

      let mutable view = Matrix.CreateLookAt(Vector3.Forward * 30.0f, Vector3.Zero, Vector3.Up)
      let projection = Matrix.CreatePerspectiveFieldOfView(1.5f, 1.6f, 0.1f, 1000.0f)
      let police_chase = PoliceChase.s0()

      do this.Content.RootDirectory <- "Content"

      (*
      We initialize the sprite batch and the basic effect.
      We also setup the basic effect so that it uses a rendering algorithm that supports textures (the TextureEnabled property) and tinting (the VertexColorEnabled property):
      *)
      override this.Initialize() = 
        do sprite_batch <- new SpriteBatch(this.GraphicsDevice)
        do basic_effect <- new BasicEffect(this.GraphicsDevice)
        do basic_effect.TextureEnabled <- true
        do basic_effect.VertexColorEnabled <- true

        base.Initialize()
  
      (*
      We load the various models and textures as we did before:
      *)
      override this.LoadContent() = 
        do police_ship    <- this.Content.Load(@"3D\A1")
        do pirate_ship    <- this.Content.Load(@"3D\A2")
        do cargo_ship     <- this.Content.Load(@"3D\A3")
        do police_station <- this.Content.Load(@"3D\station")
        do stars          <- this.Content.Load(@"3D\nightsky")
        do nebula         <- this.Content.Load(@"3D\crab_nebula")
        do laser          <- this.Content.Load(@"3D\dash")

        (*
        A model contains a series of meshes, joined together in a hierarchy.
        Each mesh contains a series of effects, which we know in this case to be instances of BasicEffect.
        We iterate the various effects, and cast them to BasicEffect with the :?> operator to invoke the EnableDefaultLighting method, which turns lighting on for our models;
        lighting on a model gives it a much more 3D appearance, by shading the surfaces of the model.
        We perform this operation for each one of the loaded models:
        *)
        let enable_lighting (m:Model) =
          for m in m.Meshes do
            for e in m.Effects do
              do (e :?> BasicEffect).EnableDefaultLighting()
        do police_ship    |> enable_lighting
        do pirate_ship    |> enable_lighting
        do cargo_ship     |> enable_lighting
        do police_station |> enable_lighting
      
        base.LoadContent()

      (*
      The Update function updates the simulation, reads the user input and rebuilds the view matrix.
      Also, the Update function resets the World, View and Projection matrices that basic_effect will use when rendering with the sprite_batch:
      *)
      override this.Update gt = 
        do PoliceChase.simulation_step(police_chase)

        let dt = gt.ElapsedGameTime.TotalSeconds |> float32
        if Keyboard.GetState().[Keys.A] = KeyState.Down then
          do zoom <- zoom * 1.01f
        elif Keyboard.GetState().[Keys.S] = KeyState.Down then
          do zoom <- zoom / 1.01f

        if Keyboard.GetState().[Keys.Left] = KeyState.Down then
          do center.X <- center.X + dt * 10.0f / (zoom * zoom)
        if Keyboard.GetState().[Keys.Right] = KeyState.Down then
          do center.X <- center.X - dt * 10.0f / (zoom * zoom)
        if Keyboard.GetState().[Keys.Up] = KeyState.Down then
          do center.Y <- center.Y + dt * 10.0f / (zoom * zoom)
        if Keyboard.GetState().[Keys.Down] = KeyState.Down then
          do center.Y <- center.Y - dt * 10.0f / (zoom * zoom)

        do view <- Matrix.CreateLookAt(Vector3.Forward * 30.0f * zoom + center, center, Vector3.Up)
        do basic_effect.World      <- Matrix.Identity
        do basic_effect.View       <- view
        do basic_effect.Projection <- projection
        base.Update gt

      (*
      When drawing, we will:
      - draw the animated background
      - draw the active lasers
      - reset some rendering parameters that the SpriteBatch sets and which are not compatible with 3D rendering
      - render the 3D models as we did in the previous section
      *)
      override this.Draw gt = 
        do this.GraphicsDevice.Clear(Color.Black)

        (*
        The first rendering operations draw the animated background.
        We use additive blending, meaning that each time we draw a texture its colors are added to the colors that are already there on the screen.
        This effectively shows all textures, one over another, where black and dark pixels appear transparent.
        *)
        do sprite_batch.Begin(SpriteSortMode.Immediate, BlendState.Additive)
        (*
        We start by drawing the starry beackground:
        *)
        do sprite_batch.Draw(
            stars,
            Vector2.Zero, 
            Nullable<_>(),
            Color.White, 
            0.0f, 
            Vector2.Zero, 
            4.0f, 
            SpriteEffects.None,
            0.0f)
        (*
        At this point we draw the nebula, slowly rotating around its center (obtained with the Width and Height properties of the Texture2D class):
        *)
        do sprite_batch.Draw(
            nebula,
            Vector2(640.0f,400.0f), 
            Nullable<_>(),
            Color.White, 
            float32 gt.TotalGameTime.TotalMinutes * 0.5f, 
            Vector2(nebula.Width |> float32, nebula.Height |> float32) * 0.5f, 
            1.0f, 
            SpriteEffects.None,
            0.0f)
        do sprite_batch.End()


        let convert_position (position:Vector2) (z:float32) =
          let field_size = float32(PoliceChase.field_size)
          let position = position / field_size
          let position = position * 2.0f - Vector2.One
          Vector3(Vector2(this.GraphicsDevice.Viewport.AspectRatio,1.0f) * 20.0f * position, z)

        (*
        Now we open a new instance of the sprite_batch;
        this new instance uses the basic_effect for rendering.
        This means that the default transformation (vertex shader) and coloring (pixel shader) algorithms of the SpriteBatch are ignored;
        instead, the algorithms contained in the last parameter (the BasicEffect) are used.
        Since basic_effect uses the same view and projection matrices of the 3D models, this means that the rendering operations we perform here draw quads in the same 3D Cartesian system of the models.
        When we zoom or move the camera, the background will remain in the same place, while what we draw in the following lines changes accordingly:
        *)
        sprite_batch.Begin(
          SpriteSortMode.Immediate,
          BlendState.Additive,
          SamplerState.LinearClamp,
          DepthStencilState.None,
          RasterizerState.CullNone,
          basic_effect)
        (*
        Drawing a laser between two points and with a certain color requires:
        - to compute the rotation of the laser, so that it goes from one point to the next (we use the atan2 function)
        - to compute the length of the laser, so that it reaches its target
        - to compute a time-varying scale and color
        - to invoke the sprite_batch Draw method
        *)
        let draw_laser (position:Vector3) (target:Vector3) (color:Color) =
          (*
          We compute the vector dir that goes from the source to the target:
          *)
          let dir = (target - position)
          (*
          dist is the length of the laser beam
          *)
          let dist = dir.Length()
          (*
          rotation is the angle between the dir vector and the X-axis:
          *)
          let rotation = atan2 dir.Y dir.X
          let laser_size = Vector2(laser.Width |> float32, laser.Height |> float32)
          let t = gt.TotalGameTime.TotalSeconds |> float32 |> ( * ) 15.0f
          do sprite_batch.Draw(
                laser, 
                Vector2(position.X,position.Y), 
                Nullable(), 
                color * (0.7f + 0.3f * sin(t + position.X)), 
                rotation, 
                Vector2(0.0f,laser_size.Y * 0.5f), 
                Vector2(dist,0.2f + cos(position.Y + t) * 0.1f) / laser_size, 
                SpriteEffects.None, 
                position.Z)
        (*
        At this point we draw all the lasers that are currently active in the scene:
        *)
        if Chapter2.Math.Vector2.Distance(police_chase.Patrol.Position, police_chase.Pirate.Position) < police_chase.Patrol.WeaponsRange then
          let position = convert_position police_chase.Patrol.Position.ToXNAVector -0.2f
          let target   = convert_position police_chase.Pirate.Position.ToXNAVector -0.2f
          if police_chase.Patrol.Integrity > 0.0<_> && 
             police_chase.Pirate.Integrity > 0.0<_> then
            do draw_laser position target Color.Blue
        if Chapter2.Math.Vector2.Distance(police_chase.Cargo.Position, police_chase.Pirate.Position) < police_chase.Cargo.WeaponsRange then
          let position = convert_position police_chase.Cargo.Position.ToXNAVector 0.0f
          let target   = convert_position police_chase.Pirate.Position.ToXNAVector 0.0f
          if police_chase.Cargo.Integrity > 0.0<_> && 
             police_chase.Pirate.Integrity > 0.0<_> then
            do draw_laser position target Color.YellowGreen
        if Chapter2.Math.Vector2.Distance(police_chase.Patrol.Position, police_chase.Pirate.Position) < police_chase.Pirate.WeaponsRange then
          let position = convert_position police_chase.Pirate.Position.ToXNAVector 0.2f
          let target   = convert_position police_chase.Patrol.Position.ToXNAVector 0.2f
          if police_chase.Patrol.Integrity > 0.0<_> && 
             police_chase.Pirate.Integrity > 0.0<_> then
            do draw_laser position target Color.Red
        elif Chapter2.Math.Vector2.Distance(police_chase.Cargo.Position, police_chase.Pirate.Position) < police_chase.Pirate.WeaponsRange then
          let position = convert_position police_chase.Pirate.Position.ToXNAVector 0.0f
          let target   = convert_position police_chase.Cargo.Position.ToXNAVector 0.0f
          if police_chase.Cargo.Integrity > 0.0<_> && 
             police_chase.Pirate.Integrity > 0.0<_> then
            do draw_laser position target Color.Red
        do sprite_batch.End()


        (*
        Models are opaque and require the Z-buffer to be on (to keep only those pixels which are nearest to the viewer).
        For this reason we reset the corresponding graphics card options that the SpriteBatch had modified:
        *)
        do this.GraphicsDevice.BlendState        <- BlendState.Opaque
        do this.GraphicsDevice.DepthStencilState <- DepthStencilState.Default

        (*
        At this point we can draw our models exactly as we did before:
        *)
        let draw_model (model:Model) (z:float32) (position:Vector2) (rotation:float32) =
          let position = convert_position position z
          do model.Draw(Matrix.CreateRotationZ(rotation - 1.5f) *
                        Matrix.CreateTranslation(position),
                        view,
                        projection)

        let draw_ship (model:Model) (z:float32) (ship:PoliceChase.Ship) =
          let rotation = (atan2 ship.Velocity.Y ship.Velocity.X) |> float32
          do draw_model model z (ship.Position.ToXNAVector) rotation

        do draw_model police_station 1.0f (police_chase.PoliceStation.Position.ToXNAVector) (float32 gt.TotalGameTime.TotalMinutes * 2.0f)
        if police_chase.Patrol.Integrity > 0.0<_> then
          do police_chase.Patrol |> draw_ship police_ship 0.0f
        if police_chase.Pirate.Integrity > 0.0<_> then
          do police_chase.Pirate |> draw_ship pirate_ship 0.0f
        if police_chase.Cargo.Integrity > 0.0<_> then
          do police_chase.Cargo  |> draw_ship cargo_ship  0.0f

        base.Draw gt
